// legacy filesystem API

import * as fs from 'fs-extra';
import * as glob from 'glob';
import { createHash } from 'crypto';
import * as vscode from 'vscode';
import { injectable } from "inversify";
import { promisify } from 'util';
import { FileSystemPaths, FileSystemPathUtils } from "./fa-paths";
import { FileStat, FileType, IFileSystem, IFileSystemPaths, IFileSystemPathUtils, IFileSystemUtils, IRawFileSystem, ITempFileSystem, ReadStream, WriteStream } from "./types";
import { TemporaryFileSystem } from './fs-temp';
import { traceError } from '../../logging';
import { isFileExistsError, isFileNotFoundError } from './error';

const ENCODING = 'utf8';
interface IRawPath {
    dirname(path: string): string;
    join(...paths: string[]): string;
}

interface IVSCodeFileSystemAPI {
    copy(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>;
    createDirectory(uri: vscode.Uri): Thenable<void>;
    delete(uri: vscode.Uri, options?: { recursive: boolean; useTrash: boolean }): Thenable<void>;
    readDirectory(uri: vscode.Uri): Thenable<[string, FileType][]>;
    readFile(uri: vscode.Uri): Thenable<Uint8Array>;
    rename(source: vscode.Uri, target: vscode.Uri, options?: { overwrite: boolean }): Thenable<void>;
    stat(uri: vscode.Uri): Thenable<FileStat>;
    writeFile(uri: vscode.Uri, content: Uint8Array): Thenable<void>;
}

// This is the parts of the 'fs-extra' module that we use in RawFileSystem.
interface IRawFSExtra {
    lstat(filename: string): Promise<fs.Stats>;
    chmod(filePath: string, mode: string | number): Promise<void>;
    appendFile(filename: string, data: unknown): Promise<void>;

    // non-async
    lstatSync(filename: string): fs.Stats;
    statSync(filename: string): fs.Stats;
    readFileSync(path: string, encoding: string): string;
    createReadStream(filename: string): ReadStream;
    createWriteStream(filename: string): WriteStream;
    pathExists(filename: string): Promise<boolean>;
}

export class RawFileSystem implements IRawFileSystem {
    constructor(
        // the low-level FS path operations to use
        protected readonly paths: IRawPath,
        // the VS Code FS API to use
        protected readonly vscfs: IVSCodeFileSystemAPI,
        // the node FS API to use
        protected readonly fsExtra: IRawFSExtra,
    ) { }
    public async move(src: string, tgt: string): Promise<void> {
        const srcUri = vscode.Uri.file(src);
        const tgtUri = vscode.Uri.file(tgt);
        // The VS Code API will automatically create the target parent
        // directory if it does not exist (even though the docs imply
        // otherwise).  So we have to manually stat, just to be sure.
        // Note that this behavior was reported, but won't be changing.
        // See: https://github.com/microsoft/vscode/issues/84177
        await this.vscfs.stat(vscode.Uri.file(this.paths.dirname(tgt)));
        // We stick with the pre-existing behavior where files are
        // overwritten and directories are not.
        const options = { overwrite: false };
        try {
            await this.vscfs.rename(srcUri, tgtUri, options);
        } catch (err) {
            if (!isFileExistsError(err)) {
                throw err; // re-throw
            }
            const stat = await this.vscfs.stat(tgtUri);
            if (stat.type === FileType.Directory) {
                throw err; // re-throw
            }
            options.overwrite = true;
            await this.vscfs.rename(srcUri, tgtUri, options);
        }
    }
    public async readText(filename: string): Promise<string> {
        const uri = vscode.Uri.file(filename);
        const result = await this.vscfs.readFile(uri);
        const data = Buffer.from(result);
        return data.toString(ENCODING);
    }
    public async stat(filename: string): Promise<vscode.FileStat> {
        const uri = vscode.Uri.file(filename);
        return this.vscfs.stat(uri);
    }
    public async pathExists(filename: string): Promise<boolean> {
        return this.fsExtra.pathExists(filename);
    }
    // Create a new object using common-case default values.
    public static withDefaults(
        paths?: IRawPath, // default: a new FileSystemPaths object (using defaults)
        vscfs?: IVSCodeFileSystemAPI, // default: the actual "vscode.workspace.fs" namespace
        fsExtra?: IRawFSExtra, // default: the "fs-extra" module
    ): RawFileSystem {
        return new RawFileSystem(
            paths || FileSystemPaths.withDefaults(),
            vscfs || vscode.workspace.fs,
            // The "fs-extra" module is effectively equivalent to node's "fs"
            // module (but is a bit more async-friendly).  So we use that
            // instead of "fs".
            fsExtra || (fs as IRawFSExtra),
        );
    }
}
// filesystem "utils"

// High-level filesystem operations used by the extension.
export class FileSystemUtils implements IFileSystemUtils {

    constructor(
        public readonly raw: IRawFileSystem,
        public readonly pathUtils: IFileSystemPathUtils,
        public readonly paths: IFileSystemPaths,
        public readonly tmp: ITempFileSystem,
        private readonly getHash: (data: string) => string,
        private readonly globFiles: (pat: string, options?: { cwd: string; dot?: boolean }) => Promise<string[]>,
    ) { }

    fileExists(filename: string): Promise<boolean> {
        return this.pathExists(filename, FileType.File);
    }

    // Create a new object using common-case default values.
    public static withDefaults(
        raw?: IRawFileSystem,
        pathUtils?: IFileSystemPathUtils,
        tmp?: ITempFileSystem,
        getHash?: (data: string) => string,
        globFiles?: (pat: string, options?: { cwd: string }) => Promise<string[]>,
    ): FileSystemUtils {
        pathUtils = pathUtils || FileSystemPathUtils.withDefaults();
        return new FileSystemUtils(
            raw || RawFileSystem.withDefaults(pathUtils.paths),
            pathUtils,
            pathUtils.paths,
            tmp || TemporaryFileSystem.withDefaults(),
            getHash || getHashString,
            globFiles || promisify(glob),
        );
    }


    // helpers

    public async pathExists(
        // the "file" to look for
        filename: string,
        // the file type to expect; if not provided then any file type
        // matches; otherwise a mismatch results in a "false" value
        fileType?: FileType,
    ): Promise<boolean> {
        if (fileType === undefined) {
            // Do not need to run stat if not asking for file type.
            return this.raw.pathExists(filename);
        }
        let stat: FileStat;
        try {
            // Note that we are using stat() rather than lstat().  This
            // means that any symlinks are getting resolved.
            stat = await this.raw.stat(filename);
        } catch (err) {
            if (isFileNotFoundError(err)) {
                return false;
            }
            traceError(`stat() failed for "${filename}"`, err);
            return false;
        }

        if (fileType === FileType.Unknown) {
            // FileType.Unknown == 0, hence do not use bitwise operations.
            return stat.type === FileType.Unknown;
        }
        return (stat.type & fileType) === fileType;
    }

}


export function getHashString(data: string): string {
    const hash = createHash('sha512');
    hash.update(data);
    return hash.digest('hex');
}

// more aliases (to cause less churn)
@injectable()
export class FileSystem implements IFileSystem {
    protected utils: FileSystemUtils;

    constructor() {
        this.utils = FileSystemUtils.withDefaults();
    }
    fileExists(filename: string): Promise<boolean> {
        return this.utils.fileExists(filename);
    }
    async move(src: string, tgt: string): Promise<void> {
        await this.utils.raw.move(src, tgt);
    }
    public async readFile(filePath: string): Promise<string> {
        return this.utils.raw.readText(filePath);
    }

}
